﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Data;
using System.Text.RegularExpressions;

namespace AutomationTools.Procedimentos.Leitura
{
    public class Excel<T> : IProcedimento
    {
        
        string filePath;
        bool usarCabecalhoComoColuna;

        public Excel(string filePath)
            : this(filePath, true)
        {

        }

        public Excel(string filePath, bool usarCabecalhoComoColuna)
        {
            this.filePath = filePath;
            this.usarCabecalhoComoColuna = usarCabecalhoComoColuna;
        }

        public object Executar(IContexto contexto)
        {
            var xls = new AutomationTools.Libs.XlsReader(filePath, usarCabecalhoComoColuna);
            return ParseObject(xls.ObterDataSet(), typeof(T));
        }

        private object ParseObject(DataSet dataSet, Type type)
        {
            DataTable dt;
            var atributos = type.GetCustomAttributes(typeof(CanExcelParseAttribute), true);
            var atributo = (CanExcelParseAttribute)atributos.GetValue(0);
            if (atributo.SheetName != null)
                dt = dataSet.Tables[string.Format("'{0}$'", atributo.SheetName)];
            else if (atributo.SheetIndex.HasValue)
                dt = dataSet.Tables[atributo.SheetIndex.Value];
            else
                throw new Exception("O DataSet não foi especificado.");

            return ParseObject(dt, type);
        }

        private object ParseObject(object dt, Type tipo)
        {
            object ret = null;
            var atributoDoTipo = (CanExcelParseAttribute)tipo.GetCustomAttributes(typeof(CanExcelParseAttribute), true)[0];
            if (atributoDoTipo.IsTable)
            {
                ArrayList arrRet = new ArrayList();
                //foreach (DataRow rows in ((DataTable)dt).Rows)
                for (int i = atributoDoTipo.HasCabecalho ? 1 : 0 ; i < ((DataTable)dt).Rows.Count; i++)
                {
                    DataRow rows = ((DataTable)dt).Rows[i];
                    ret = PopularObjeto(rows, tipo);
                    arrRet.Add(ret);
                }
                return arrRet;
            }
            else
            {
                ret = PopularObjeto(dt, tipo);
            }
                    
            return ret;
        }


        private static object ParseObject(IEnumerable<object> item, Type tipo)
        {
            object ret = Activator.CreateInstance(tipo);
            foreach (var property in tipo.GetProperties())
            {
                var atributos = property.GetCustomAttributes(typeof(ExcelParseAttribute), true);
                if (atributos.Length == 0) continue;
                var atributo = (ExcelParseAttribute)atributos.GetValue(0);

                if (item.Count() <= atributo.IndiceColuna)
                    throw new Exception(string.Format("O índice '{0}' está fora do numero de colunas que a tabela possui.", atributo.IndiceColuna));
                if (!atributo.IndiceColuna.HasValue)
                    throw new Exception(string.Format("A propriedade IndiceColuna atributo ExcelParse deve estar preenchido para a propriedade '{0}'", property.Name));

                AtribuirValorAoObjeto(property, item.ToArray()[atributo.IndiceColuna.Value], ret);
            }
            return ret;
        }

        private object PopularObjeto(object fonte, Type tipo)
        {
            object ret = Activator.CreateInstance(tipo);
            foreach (var property in tipo.GetProperties())
            {
                var atributos = property.GetCustomAttributes(typeof(ExcelParseAttribute), true);
                if (atributos.Length == 0) continue;
                var atributo = (ExcelParseAttribute)atributos.GetValue(0);

                var atributosProperty = property.PropertyType.GetCustomAttributes(typeof(CanExcelParseAttribute), true);

                if (atributo.IsArray)
                {
                    if (!(fonte is DataTable))
                        throw new Exception("Uma marcada com IsTable=true, não pode conter atributos marcados como IsArray.");

                    var elementType = property.PropertyType.GetElementType();
                    ArrayList arr = new ArrayList();

                    foreach (var item in CropDataTable((DataTable)fonte, atributo.CellRegionInicio, atributo.CellRegionFim))
                    {
                        arr.Add(ParseObject(item, elementType));
                    }
                    AtribuirValorAoObjeto(property, arr.ToArray(elementType), ret);
                }
                else
                {
                    if (atributosProperty.Length > 0)
                    {
                        if (!(fonte is DataTable))
                            throw new Exception("Uma marcada com IsTable = true, não pode conter propriedades de um tipo marcado com CanExcelParseAttribute.");

                        property.SetValue(ret, ParseObject(fonte, property.PropertyType), null);
                    }
                    else
                    {
                        object valor = ObterValorPeloCell(fonte, atributo.Cell);
                        AtribuirValorAoObjeto(property, valor, ret);
                    }
                }

            }
            return ret;
        }

        private static void AtribuirValorAoObjeto(System.Reflection.PropertyInfo propriedade, object valor, object instancia)
        {
            object temp = null;
            try
            {
                temp = Convert.ChangeType(valor, propriedade.PropertyType);
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("Erro ao converter o valor '{0}' no tipo '{1}', para a propriedade '{2}'. Verifique a propriedade IsTable do atributo CanExcelParse.", valor, propriedade.PropertyType.Name, propriedade.Name), ex);
            }
            propriedade.SetValue(instancia, temp, null);
        }

        private ListaMultiDim<object> CropDataTable(DataTable dt, string cellIni, string cellFim)
        {
            ListaMultiDim<object> ret = new ListaMultiDim<object>();
            var linhaColunaIni = ConverterExcelToLinhaColuna(cellIni);
            var linhaColunaFim = ConverterExcelToLinhaColuna(cellFim);

            var linhaMax = (linhaColunaFim.Linha > dt.Rows.Count || !linhaColunaFim.Linha.HasValue) ? dt.Rows.Count : linhaColunaFim.Linha;
            var colunaMax = linhaColunaFim.Coluna > dt.Columns.Count ? dt.Columns.Count : linhaColunaFim.Coluna;


            for (int i = linhaColunaIni.Linha.Value; i < linhaMax; i++)
            {
                var linha = new List<object>();
                for (int j = linhaColunaIni.Coluna; j <= colunaMax; j++)
                {
                    linha.Add(dt.Rows[i][j]);
                }
                ret.Add(linha);
            }

            return ret;
        }

        private object ObterValorPeloCell(object fonte, string cell)
        {
            if (fonte is DataTable)
            {
                var dt = fonte as DataTable;
                var linhaColuna = ConverterExcelToLinhaColuna(cell);
                return dt.Rows[linhaColuna.Linha.Value][linhaColuna.Coluna];
            }
            else if (fonte is DataRow)
            {
                var dr = fonte as DataRow;
                var linhaColuna = ConverterExcelToLinhaColuna(cell);
                if (dr.ItemArray.Length <= linhaColuna.Coluna)
                    return null;
                return dr[linhaColuna.Coluna];
            }else
                return null;
        }

        private StructLinhaColuna ConverterExcelToLinhaColuna(string cell)
        {
            var rColuna = new Regex(@"^[A-z]+").Match(cell).Value;
            var rLinha = new Regex(@"\d+$").Match(cell).Value;
            int? linha = null;
            if (!string.IsNullOrEmpty(rLinha))
                linha = int.Parse(rLinha) - 1;
            var coluna = NumberFromExcelColumn(rColuna) - 1;

            return new StructLinhaColuna()
            {
                Linha = linha,
                Coluna = coluna
            };
        }


        private struct StructLinhaColuna
        {
            public int? Linha;
            public int Coluna;
        }


        

        public void ValidarEntrada(IContexto contexto)
        {
            throw new NotImplementedException();
        }


        /// <summary>
        /// 1 -> A<br/>
        /// 2 -> B<br/>
        /// 3 -> C<br/>
        /// ...
        /// </summary>
        /// <param name="column"></param>
        /// <returns></returns>
        public static string ExcelColumnFromNumber(int column)
        {
            string columnString = "";
            decimal columnNumber = column;
            while (columnNumber > 0)
            {
                decimal currentLetterNumber = (columnNumber - 1) % 26;
                char currentLetter = (char)(currentLetterNumber + 65);
                columnString = currentLetter + columnString;
                columnNumber = (columnNumber - (currentLetterNumber + 1)) / 26;
            }
            return columnString;
        }

        /// <summary>
        /// A -> 1<br/>
        /// B -> 2<br/>
        /// C -> 3<br/>
        /// ...
        /// </summary>
        /// <param name="column"></param>
        /// <returns></returns>
        public static int NumberFromExcelColumn(string column)
        {
            int retVal = 0;
            string col = column.ToUpper();
            for (int iChar = col.Length - 1; iChar >= 0; iChar--)
            {
                char colPiece = col[iChar];
                int colNum = colPiece - 64;
                retVal = retVal + colNum * (int)Math.Pow(26, col.Length - (iChar + 1));
            }
            return retVal;
        }
    }

    public class ListaMultiDim<T> : List<List<T>> { }

    /// <summary>
    /// Atributo que será utilizado na conversão do texto em classe.
    /// </summary>
    [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = false)]
    public sealed class ExcelParseAttribute : Attribute
    {
        public ExcelParseAttribute(string cellRegionInicio, string cellRegionFim)
        {
            CellRegionInicio = cellRegionInicio;
            CellRegionFim = cellRegionFim;
        }

        public ExcelParseAttribute(string cell)
        {
            this.Cell = cell;
        }

        public ExcelParseAttribute(int indiceColuna)
        {
            this.IndiceColuna = indiceColuna;
        }

        public int? IndiceColuna { get; set; }

        public string CellRegionInicio
        {
            get;
            set;
        }

        public string CellRegionFim
        {
            get;
            set;
        }

        public string Cell
        {
            get;
            set;
        }

        /// <summary>
        /// Indica que o resultado da Regex será convertido em um array de objetos.
        /// </summary>
        public bool IsArray { get; set; }

    }

    /// <summary>
    /// Define um tipo que pode ser utilizado no RegexParseObject.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
    public sealed class CanExcelParseAttribute : Attribute
    {
        public CanExcelParseAttribute() {}
        public CanExcelParseAttribute(int sheetIndex)
        {
            this.SheetIndex = sheetIndex;
        }

        public CanExcelParseAttribute(string sheetName)
        {
            this.SheetName = sheetName;
        }

        public string SheetName { get; set; }
        public int? SheetIndex { get; set; }

        bool _IsTable = true;
        public bool IsTable
        {
            get
            {
                return this._IsTable;
            }
            set
            {
                this._IsTable = value;
            }
        }

        bool _HasCabecalho = true;
        public bool HasCabecalho
        {
            get
            {
                return this._HasCabecalho;
            }
            set
            {
                this._HasCabecalho = value;
            }
        }
    }
}
